package zgps

import "math"

/*
在处理基于经纬度的多边形电子围栏时，核心挑战是球面坐标与平面计算的适配。由于经纬度是球面坐标（角度单位），
直接用平面几何算法（如射线法）会有误差，需先通过地图投影将经纬度转换为平面坐标（米制），再进行点与多边形的关系判断。以下是详细原理和实现代码：
一、原理说明
经纬度的特殊性经纬度（纬度：-90°~90°，经度：-180°~180°）是球面坐标，单位是角度，直接计算距离或判断位置关系会因地球曲率产生误差。
例如，1° 经度在赤道约为 111km，在北极点附近接近 0km。
解决方案：墨卡托投影墨卡托投影（Mercator Projection）是一种常用的地图投影方式，能将球面经纬度转换为平面直角坐标（x、y，单位米），
且在小范围内（如城市级围栏）误差可接受。转换后可直接使用平面几何算法（如射线法）判断点是否在多边形内。
核心步骤
步骤 1：将多边形围栏的所有经纬度顶点转换为墨卡托平面坐标。
步骤 2：将待判断点的经纬度也转换为墨卡托平面坐标。
步骤 3：用射线法判断平面点是否在平面多边形内。
*/

// 常量定义（地球半径，单位：米）
const earthRadius = 6378137.0 // WGS84椭球体半长轴，墨卡托投影常用此值

// LonLat 表示经纬度坐标（经度：longitude，纬度：latitude）
type LonLat struct {
	Lon float64 // 经度（°），范围：-180~180
	Lat float64 // 纬度（°），范围：-90~90
}

// Mercator 表示墨卡托平面坐标（x、y单位：米）
type Mercator struct {
	X float64 // 对应经度转换后的x坐标
	Y float64 // 对应纬度转换后的y坐标
}

// PolygonFence 多边形电子围栏（基于经纬度）
type PolygonFence struct {
	Points []LonLat // 围栏顶点（经纬度），需按顺时针/逆时针排序，建议首尾闭合
}

// ToMercator 经纬度转墨卡托平面坐标
// 公式参考：https://epsg.io/3857（Web Mercator投影标准）
func toMercator(ll LonLat) Mercator {
	// 纬度转换（处理南北极附近的极限情况，纬度范围限制在±85.0511287798°）
	latRad := ll.Lat * math.Pi / 180.0 // 纬度转弧度
	latRad = math.Max(math.Min(latRad, 1.4844222297453223), -1.4844222297453223)

	// 墨卡托y坐标计算公式：y = R * ln(tan(π/4 + lat/2))
	y := earthRadius * math.Atanh(math.Sin(latRad))

	// 墨卡托x坐标计算公式：x = R * lon（弧度）
	lonRad := ll.Lon * math.Pi / 180.0 // 经度转弧度
	x := earthRadius * lonRad

	return Mercator{X: x, Y: y}
}

// 射线法判断平面点是否在平面多边形内
// 输入：待判断的墨卡托点p，多边形顶点的墨卡托坐标集合poly
// 原理：从p向右发射射线，统计与多边形边的交点数，奇数则在内部，偶数则在外部
func isPointInPolygon(p Mercator, poly []Mercator) bool {
	n := len(poly)
	if n < 3 {
		return false // 多边形至少需要3个顶点
	}
	inside := false

	// 遍历多边形的每条边（i是当前顶点索引，j是上一个顶点索引）
	for i, j := 0, n-1; i < n; j, i = i, i+1 {
		// 当前边的两个顶点：a（i）和b（j）
		a := poly[i]
		b := poly[j]

		// 1. 检查点p的y坐标是否在边a-b的y范围内（排除不相交的边）
		// (a.Y > p.Y) != (b.Y > p.Y) 表示p的y在a和b的y之间
		if (a.Y > p.Y) != (b.Y > p.Y) {
			// 2. 计算射线（y=p.Y，x>p.X）与边a-b的交点x坐标
			// 边a-b的斜率：k = (a.X - b.X)/(a.Y - b.Y)
			// 交点x = b.X + (p.Y - b.Y) * k
			slope := (a.X - b.X) / (a.Y - b.Y)
			xIntersect := b.X + (p.Y-b.Y)*slope

			// 3. 若交点在p的右侧（xIntersect > p.X），则射线与边相交
			if p.X < xIntersect {
				inside = !inside // 交点数为奇数时，状态翻转
			}
		}
	}
	return inside
}

// Contains 判断点是否在多边形围栏内（对外暴露的方法）
func Contains(points []LonLat, mePoint LonLat) bool {
	// 步骤1：将围栏所有顶点转换为墨卡托坐标
	var mercatorPoly []Mercator
	for _, ll := range points {
		// 多形边坐标
		mercatorPoly = append(mercatorPoly, toMercator(ll))
	}

	// 步骤2：将待判断点转换为墨卡托坐标(物体坐标)
	mercatorPoint := toMercator(mePoint)

	// 步骤3：用射线法判断
	return isPointInPolygon(mercatorPoint, mercatorPoly)
}
